CEngine_Damage = 1

function CEngine_initialise() --Core Initialisation Function
	Msg("Contraption Engine: Initialising Damage Engine\n")
	CDamage_initcvars() --Initialise convars
	CDamage_inittags() --Create Server Tags
	include("autorun/shared/cengine_effects.lua")
	AddCSLuaFile("autorun/shared/cengine_effects.lua")
	AddCSLuaFile("autorun/client/cengine_clienteventslibrary.lua")
	AddCSLuaFile("autorun/client/cengine_shieldrenderlibrary.lua")
	CDamage = 1
	Msg("Contraption Engine: Damage Engine Init Finished\n")
end

function CDamage_initcvars() --CDamage Convar Initialisation. Initialises Systems Convars
	Msg("Contraption Damage Engine: Initialising Server Convars\n")
	local CVAR_FILE = "contraption_engine/damageengine_cvars.txt"
	if (!file.Exists(CVAR_FILE)) then
		ErrorNoHalt("Contraption Damage Engine: Convar Initialisation Error - CVar File Does Not Exist\n")
	else
		local varfile = file.Read( CVAR_FILE )
		CVars = util.KeyValuesToTable(varfile)
		for k, v in pairs(CVars) do
			if (!ConVarExists(k)) then
				CreateConVar(k, v, FCVAR_NOTIFY | FCVAR_ARCHIVE | FCVAR_REPLICATED)
			end
		end
		Msg("Contraption Damage Engine: CVar Initialisation Completed Successfully\n")
	end
end

function CDamage_inittags()
	local tags = GetConVarString("sv_tags")

	if tags == nil then
		RunConsoleCommand("sv_tags", "CDE 0.9a")
	elseif not string.find(tags, "CDE") then
		RunConsoleCommand("sv_tags", tags .. ",CDE 0.9a")
	end
end

CEngine_initialise()

function CDamage_newentity(entity) --Initialises new CDamage engine object (Internal Function. Called Automatically)
	if (entity:IsValid() and entity:GetPhysicsObject():IsValid() and CEngine_Damage and CDamage_checkentityvalidity(entity))
	then
		local objectmass = entity:GetPhysicsObject():GetMass()
		if entity:GetClass() == "gmod_hoverball" or entity:GetClass() == "gmod_wire_hoverball" then objectmass = 1 end --Make hoverballs 1HP instead of 3000HP
		entity.CDamage = entity.CDamage or {}
		entity.CDamage.valid = 1
		entity.CDamage.maxhealth = entity.CDamage.maxhealth or math.Clamp((objectmass * GetConVarNumber("sv_CDamage_enthealthmultiplier")), 1, GetConVarNumber("sv_CDamage_maxenthealth"))
		entity.CDamage.health = entity.CDamage.health or entity.CDamage.maxhealth
		--entity.CDamage.radioactivity = entity.CDamage.radioactivity or 0
		--entity.CDamage.radiationhalflife = entity.CDamage.radiationhalflife or 0
		--This feature is not yet implemented. I have commented it out until such time as radioactivity is implemented
		entity.CDamage.isshielded = false
		entity.CDamage.shield = nil
		entity.CDamage.cloaked = false
		entity:SetNetworkedBool("IsShielded", false)
		entity.CDamage.volatility = entity.CDamage.volatility or 0
		entity.CDamage.immune = false --LEGACY PROPERTY. THIS WILL BE REMOVED ONCE I WORK OUT WHAT'S CALLING IT AND REPLACED WITH TYPE IMMUNITIES
		entity.CDamage.type = entity.CDamage.type or "prop"
		CDamage_EntityInit_Extras(entity)
	end
end

function CDamage_checkentityvalidity(entity) --Checks if an object is a valid damage object. Prevents world objects and otherwise non-physical entities from being affected by the engine (Internal Function. Called Automatically)
	local valid = true
	local class = entity:GetClass()
	if entity:IsPlayer() then valid = false end
	if entity:IsNPC() then valid = false end
	if class == "gmod_ghost" then valid = false end
	if string.find(class, "env_") == 1 then valid = false end
	if string.find(class, "info_") == 1 then valid = false end
	if string.find(class, "func_") == 1 then valid = false end
	if (string.find(class, "phys_") == 1) and (string.find(class, "phys_magnet") == 0) then valid = false end
	if string.find(class, "logic_") == 1 then valid = false end
	if class == "prop_door_rotating" then valid = false end --Thanks to {TS} Reaper for finding this one!
	return valid
end
hook.Add("OnEntityCreated", "CDamage_entityinit", function(entity) timer.Simple(0.1, CDamage_newentity, entity) end)

function CDamage_damageent(entity, damage, player, damagetype) --Damages an entity. THIS IS THE MAIN DIRECT DAMAGE-DEALING FUNCTION
																--entity: (Entity) the entity to damage
																--damage: (Integer) the amount to damage the specified entity
																--player: (Player) the player causing the damage. Included for Future Implementation
																--damagetype: (String) the type of damage being done
	
	if (entity:IsValid() and entity.CDamage != nil and (entity.CDamage.valid or false)) and not entity.CDamage.immune then
		damagetype = damagetype or "normal"
		if ((CDamage[entity.CDamage.type] != nil) and (CDamage[entity.CDamage.type][damagetype] != nil)) then
			CDamage[entity.CDamage.type][damagetype](entity, damage, player)
		end
	elseif (entity:IsPlayer() or entity:IsNPC()) then
		if (entity:IsPlayer() and entity:InVehicle()) then
			CDamage_damageent(entity:GetVehicle(), damage, player, damagetype)
		elseif (CDamage["organic"][damagetype] != nil) then
			CDamage["organic"][damagetype](entity, damage, player)
		end
	end		
end

function CDamage_blastdamage(position, radius, damage, player, damagetype) --Causes damage in an area. Scaled linearly with distance (Will eventually be adding the option for linear or exponential damage scaling). THIS IS THE MAIN AREA-EFFECT DAMAGE-DEALING FUNCTION
																			--position: (Vector) the origin of the blast
																			--radius: (Integer) the radius of the blast
																			--damage: (Integer) the damage of the blast at the origin
																			--player: (Player) the player causing the damage. Included for Future Implementation
																			--damagetype: (String) The Type of Damage to inflict. Default is 'normal'
	damagetype = damagetype or "normal"
	local affected = ents.FindInSphere(position, radius)
	local validhits = {}
	local shieldsonfield = ents.FindByClass("shield_dome")
	local shieldsinproximity = {}
	for _,shield in pairs(shieldsonfield) do
		if (shield:GetPlayer() != player) and (position - shield:GetPos()):Length() <= (radius + shield.Radius) then
			table.insert(shieldsinproximity, shield)
		end
	end
	for key,entity in pairs(affected) do
		local distance
		local direction = (position - entity:GetPos()):GetNormal()
		if (entity:GetClass() == "shield_dome" and entity:GetPlayer() != player) then
			distance = CDamage_shieldintersectdistance(position, direction, entity)
		else
			distance = (position - entity:GetPos()):Length()
		end
		local blocked = false
		for _,shield in pairs(shieldsinproximity) do
			if CDamage_checkshieldintersect(position, direction, player, shield) and (CDamage_shieldintersectdistance(position, direction, shield) < distance) then
				blocked = true
				break
			end
		end
		if not blocked then
			local scaleddamage = math.Clamp(damage * ((radius - distance) / radius), damage / 10, damage)
			if (entity:GetClass() == "shield_dome" and entity:GetPlayer() != player) then
				local pointofimpact = (direction * distance) + entity:GetPos()
				entity:Impact(pointofimpact)
				CDamage_damageent(entity, scaleddamage, player, damagetype)
			else
				CDamage_damageent(entity, scaleddamage, player, damagetype) 
			end
		end
	end
end

function CDamage_repairent(entity, amount) --Repairs the given entity by the given amount.
											--entity: (Entity) The entity to repair
											--amount: (Float) The amount to repair
	if (util.tobool(GetConVarNumber("sv_CDamage_contraptiondamage"))) then
		local contraption = CDE_FindContraption( entity )
		CDamage_repaircontraption(contraption, amount)
	else
		entity.CDamage.health = math.min(entity.CDamage.health + amount, entity.CDamage.maxhealth)
	end
end

function CDamage_repaircontraption(entities, amount) --Repairs the given contraption by the given amount
														--entities: (Table of Entities) the contraption to repair
														--amount: (Float) the amount to repair
	for key,entity in pairs(entities) do
		if amount <= 0 then return end
		local repairamount = math.min(entity.CDamage.maxhealth - entity.CDamage.health, amount)
		entity.CDamage.health = entity.CDamage.health + repairamount
		amount = amount - repairamount
	end
end

function CDamage_findcontraptionhealth(entities) --Finds the total health of a contraption
													--entities: (Table of Entities) the contraption to find the health of
	local health = 0
	for _,entity in pairs(entities) do
		if (entity:IsValid() and entity.CDamage and entity.CDamage.valid) then
			health = health + entity.CDamage.health
		end
	end
	return health
end

function CDamage_findcontraptionmaxhealth(entities) --Finds the maximum health of a contraption
														--entities: (Table of Entities) the contraption to find the maximum health of
	local maxhealth = 0
	for _,entity in pairs(entities) do
		if (entity:IsValid() and entity.CDamage and entity.CDamage.valid) then
			maxhealth = maxhealth + entity.CDamage.maxhealth
		end
	end
	return maxhealth
end

function CDamage_damagecontraption(entities, damage, player) --Damages a contraption (Internal. Called by direct damage functions)
																--entities: (Table of Entities) the contraption to damage
																--damage: (Integer) the amount of damage to inflict
																--player: (Player) the player causing the damage. Included for Future Implementation
	for _,Entity in pairs(entities) do
		if (Entity.CDamage == nil) then
			CDamage_newentity(Entity)
		end
		if (Entity.CDamage.health > damage) then
			Entity.CDamage.health = Entity.CDamage.health - damage
			damage = 0
		elseif (Entity.CDamage.health == damage) then
			Entity.CDamage.health = 0
			damage = 0
		else
			damage = damage - Entity.CDamage.health
			Entity.CDamage.health = 0
		end
	end
	if (damage > 0) then
		CDamage_destroycontraption(entities, player)
	end
end

function CDamage_destroyentity(entity, player) --Destroys a specific entity (Internal. Called to cause death explosions)
													--entity: (Entity) the entity to destroy
													--player: (Player) the player causing the damage. Included for Future Implementation
	if entity:IsValid() then
		if not util.tobool(GetConVarNumber("sv_CDamage_contraptiondamage")) then
			CDamage_DeathExplosion_helper(entity)
		end
		entity:Remove()
	end
end

function CDamage_wreckentity(entity, player) --Wrecks a specific entity, removing the original entity and replacing it with a wreckage piece (Internal. Called to cause wreckage)
												--entity: (Entity) the entity to wreck
												--player: (Player) the player causing the damage. Included for Future Implementation
	local wreck = ents.Create("wreckage")
	CDamage_applywreckmodifiers(entity, wreck)
	CDamage_destroyentity(entity, player)
	wreck:Spawn()
	return wreck
end

function CDamage_applywreckmodifiers(entity, wreck) --Heavily updated wreck replication function

	--Appearance
	wreck:SetModel(entity:GetModel())
	wreck:SetSkin(entity:GetSkin())
	wreck:SetMaterial(entity:GetMaterial())
	
	--Location
	wreck:SetPos(entity:GetPos())
	wreck:SetAngles(entity:GetAngles())
	
	--Physics
	if (entity:GetPhysicsObject() != NULL) then
		wreck.mass = entity:GetPhysicsObject():GetMass()
		wreck.velocity = entity:GetPhysicsObject():GetVelocity()
		if entity:GetPhysicsObject():IsAsleep() then wreck.ToWake = false else wreck.ToWake = true end
		wreck.motion = entity:GetPhysicsObject():IsMoveable()
		if (entity.BoneMods != nil) then
			wreck.gravity = entity.BoneMods[0].physprops.GravityToggle
			wreck.material = entity.BoneMods[0].physprops.Material
		end
		if (entity.gravity != nil) then wreck.gravity = util.tobool(entity.gravity) end --For better conpatibility with spacebuild
	end
end

function CDamage_destroycontraption(entities, player) --Destroys a contraption. (Internal)
														--entities: (Table of Entities) the contraption to destroy
														--player: (Player) the player causing the damage. Included for Future Implementation
	for _,entity in pairs(entities) do
		entity.CDamage.immune = true
	end
	local contraptionmaxhealth = 0
	local Vmax, Vmin
	for _,Entity in pairs(entities) do
		if (Vmax == nil) or (Vmin == nil) then
			Vmax = Entity:GetPos()
			Vmin = Entity:GetPos()
		end
		contraptionmaxhealth = contraptionmaxhealth + Entity.CDamage.maxhealth
		local pos = Entity:GetPos()
		if (pos.x > Vmax.x) then
			Vmax.x = pos.x
		end
		if (pos.x < Vmin.x) then
			Vmin.x = pos.x
		end
		if (pos.y > Vmax.y) then
			Vmax.y = pos.y
		end
		if (pos.y < Vmin.y) then
			Vmin.y = pos.y
		end
		if (pos.z > Vmax.z) then
			Vmax.z = pos.z
		end
		if (pos.z < Vmin.z) then
			Vmin.z = pos.z
		end
	end
	local Centre = (Vmax + Vmin) / 2
	if util.tobool(GetConVarNumber("sv_CDamage_epiccontraptiondeath")) then --epic death explosions go here (Damage = Contraption health)
		CDamage_killcontraption(entities, player, true, Centre, contraptionmaxhealth)
	else --standard death explosions
		CDamage_killcontraption(entities, player, false, Centre, contraptionmaxhealth)
	end
end

function CDamage_killcontraption(entities, player, obliterate, centre, contraptionmaxhealth) --Kills a contraption . Used as a helper function for some of the more advanced damage effects (Internal)
																										--entities: (Table of Entities) the contraption to wreck
																										--player: (Player) the player causing the damage. Included for Future Implementation
																										--obliterate: (Boolean) if true, will completely remove the contraption without leaving wreckage (it will still explode). Used to calculate death explosions
																										--centre: (Vector) The calculated centre of the old contraption
																										--contraptionmaxhealth: (Integer) The original maximum health of the contraption
	for _,entity in pairs(entities) do
		entity.CDamage.immune = true
	end
	local disintegrate = util.tobool(GetConVarNumber("sv_CDamage_forcewreckfragementation"))
	local contraptionsize = 0
	for _,entity in pairs(entities) do
		local vector = entity:LocalToWorld(entity:OBBMaxs()) - centre
		if CDamage_Vector_Length_Compare(vector, contraptionsize) == 1 then
			contraptionsize = vector:Length()
		end
	end
	if (util.tobool(GetConVarNumber("sv_CDamage_delayeddeathexplosion"))) then
		local TTD = contraptionmaxhealth / (2000 / GetConVarNumber("sv_CDamage_delayeddeathexplosion"))
		local explosionsremaining = TTD * 2
		local explosionsperpiece = (math.ceil((TTD*2)/#entities))
		for _,entity in pairs(entities) do
			for i=1,explosionsperpiece do
				if (explosionsremaining > 0) then
					timer.Simple(math.Rand(0, TTD), function(target) CDEffect_explosion_death_secondary(target)  end, entity)
					explosionsremaining = explosionsremaining - 1
				end
			end
		end
		CDamage_volatileexplosion_helper(TTD + 0.1, centre, obliterate, contraptionmaxhealth, contraptionsize)
		timer.Simple(TTD, CDamage_wreckcontraption, entities, player, obliterate, disintegrate, centre)
	else
		CDamage_volatileexplosion_helper(0.1, centre, obliterate, contraptionmaxhealth, contraptionsize)
		CDamage_wreckcontraption(entities, player, obliterate, disintegrate, centre)
	end
end

function CDamage_DeathExplosion_helper(entity)
	if (entity:IsValid()) then
		CDEffect_explosion_primitive(entity:GetPos())
	end
end

function CDamage_volatileexplosion_helper(delay, Centre, obliterate, contraptionmaxhealth, contraptionsize)
	if obliterate then
		local function epicdeath_dispatch()
			local size = contraptionsize * 4
			CDamage_blastdamage(Centre, size, contraptionmaxhealth, nil)
			CDEffect_explosion_death_epic(Centre, contraptionsize, size)
		end
		timer.Simple(delay, epicdeath_dispatch)
	else
		local function death_dispatch()
			CDEffect_explosion_death_standard(Centre, contraptionsize)
		end
		timer.Simple(delay, death_dispatch)
	end
end

function CDamage_wreckcontraption(entities, player, obliterate, disintegrate, centre) --Wrecks a contraption (Internal)
																								--entities: (Table of Entities) the contraption to wreck
																								--player: (Player) the player causing the damage. Included for Future Implementation
																								--obliterate: (Boolean) if true, will completely remove the contraption without leaving wreckage (it will still explode)
																								--disintegrate: (Boolean) if true, this will not produce a contrained wreck (it will still leave wreckage)
																								--centre: (Vector) The calculated centre of the old contraption
	if util.tobool(GetConVarNumber("sv_CDamage_wreckage")) and not obliterate then
		local wreckage = {}
		local debris = {}
		local averagemass, fullmass = 0, 0
		local wreckintegrity = GetConVarNumber("sv_CDamage_wreckageintegrity")
		for _,entity in pairs(entities) do
			if (entity:IsValid()) then
				fullmass = fullmass + entity.CDamage.maxhealth
			end
		end
		averagemass = fullmass / #entities
		local cutoff1 = (2 * GetConVarNumber("sv_CDamage_debrisviolence"))
		local cutoff2 = (10 * GetConVarNumber("sv_CDamage_debrisviolence"))
		for _,entity in pairs(entities) do
			if entity:IsValid() and entity:GetPhysicsObject():IsValid() and (entity:GetSolid() == 6) then --Prevents entities with nonsolid physics from being considered for wrecking (Added due to compatibility problems with Avon's Staff Weapon)
				if entity:IsVehicle() then
					local passenger = entity:GetDriver() or entity:GetPassenger()
					if (passenger:IsValid() and passenger:IsPlayer()) then passenger:Kill() end
				end	
				if 	(disintegrate) then
					if (entity.CDamage.maxhealth >= averagemass / cutoff2) then --Little bits of debris that fly off
						table.insert(debris, CDamage_wreckentity(entity, player))
					else --Tiny bits that pose a lag risk (wire parts, for example)
						entity:Remove() 
					end
				else
					if (entity.CDamage.maxhealth >= averagemass / cutoff1) then --Main Wreckage parts
						table.insert(wreckage, CDamage_wreckentity(entity, player))
					elseif (entity.CDamage.maxhealth >= averagemass / cutoff2) then --Little bits of debris that fly off
						table.insert(debris, CDamage_wreckentity(entity, player))
					else --Tiny bits that pose a lag risk (wire parts, for example)
						entity:Remove() 
					end
				end
			end
		end
		if (wreckintegrity == -1) then
			for index1,wreck1 in pairs(wreckage) do
				for index2,wreck2 in pairs(wreckage) do
					if (index1 != index2) then
						timer.Simple(0.01, constraint.Weld, wreck1, wreck2, 0, 0, 0, true)
					end
				end
				if (wreck1:IsValid()) and (wreck1.ToWake or false) then wreck1:GetPhysicsObject():Wake() else wreck1:GetPhysicsObject():Sleep() end
			end
		else
			for index,wreck in pairs(wreckage) do
				for target=1+index, wreckintegrity do
					if wreckage[target] != nil then timer.Simple(0.01, constraint.Weld, wreck, wreckage[target], 0, 0, 0, true) end
				end
				if (wreck:IsValid()) then wreck:GetPhysicsObject():Wake() end
			end
		end
		for index,debrispiece in pairs(debris) do
			if debrispiece:IsValid() then
				local explosionvector = (debrispiece:GetPos() - centre):GetNormal() * 4000 * debrispiece:GetPhysicsObject():GetMass()
				debrispiece:GetPhysicsObject():ApplyForceOffset(explosionvector, centre)
				--debrispiece:GetPhysicsObject():SetVelocity(explosionvector)
				debrispiece:Ignite(10, 0)
				debrispiece:GetPhysicsObject():Wake()
			end
		end
	else
		for _,entity in pairs(entities) do
			CDamage_destroyentity(entity, player)
		end
	end
end
